package com.altio.swing;


import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;

import javax.swing.JLayeredPane;
import javax.swing.SwingUtilities;

/**
 * Extension of JLayeredPane where components can be added to any layer as draggable components. The user can then drag
 * these components with the mouse. Somewhat like a JDesktopPane without the need for JInternalFrames.
 * 
 * @author tomm
 * @altioapi
 * 
 */
public class DraggableLayeredPane extends JLayeredPane implements MouseMotionListener, KeyListener {
  public static final int      NORTH_EAST          = Cursor.NE_RESIZE_CURSOR;

  public static final int      NORTH_WEST          = Cursor.NW_RESIZE_CURSOR;

  public static final int      NORTH               = Cursor.N_RESIZE_CURSOR;

  public static final int      WEST                = Cursor.W_RESIZE_CURSOR;

  public static final int      EAST                = Cursor.E_RESIZE_CURSOR;

  public static final int      SOUTH_EAST          = Cursor.SE_RESIZE_CURSOR;

  public static final int      SOUTH_WEST          = Cursor.SW_RESIZE_CURSOR;

  public static final int      SOUTH               = Cursor.S_RESIZE_CURSOR;

  public static final int      NONE                = Cursor.DEFAULT_CURSOR;

  /** The component that is currently being dragged by the user, null if the user isn't dragging */
  private Component            dragComponent       = null;

  /** The point on the graph that the current drag component has been dragged to */
  private Point                currentDragPoint    = new Point();
  /** The offset between the drag component and the mouse */
  private Point                initialDragOffset   = new Point();
  private Point                currentDragOffset   = new Point();

  /** Whether or not we are dragging right now */
  private boolean              currentlyDragging   = false;

  private DragRedirecter       dragRedirecter      = new DragRedirecter();

  // TM This was a focus hack for netviz, I'm commenting it out for now as Altio doesn't need it
  // and it doesn't represent the functionality implied by this class
  /** Listener that gives focus to components that are clicked on */
  // private FocusMousePressListener focusMouseListener = new FocusMousePressListener();
  /**
   * Can be NORTH, SOUTH etc. Represents corner of the current component that we are resizing that we are dragging. NONE
   * whilst we are not resizing
   */
  private int                  resizingMode        = NONE;

  /** Vector of components that are actually resizable */
  private Vector               resizableComponents = new Vector();

  /** Vector of components that are actually draggable */
  private Vector               draggableComponents = new Vector();

  /** Manages which controls are selected */
  private SelectionManager     selectionManager;

  /**
   * Layer that each component is on usually. We store this because we move dragged components to front and back again
   */
  private HashMap              componentLayers     = new HashMap();

  /** Constraints that describe the behaviour when dragging the components */
  private DraggableConstraints constraints         = DraggableConstraints.DEFAULT_CONSTRAINTS;

  /** The x co-ordinate of the line to be drawn if we have snapped to an x value */
  private int                  snapLineX           = -1;

  /** The y co-ordinate of the line to be drawn if we have snapped to a y value */
  private int                  snapLineY           = -1;
  
  /** We cache the image of grid guidelines as they are expensive to paint on every frame */
  private Image gridCache = null;
  /** Remember what color we drew the grid so if the color changes we can redraw it */
  private Color cachedGridColor = null;
  /** Remember what size we drew the grid cells so if it changes we can redraw it */
  private int cachedGridSize = 0;
  
  /** Redirects keys to this component */
  private KeyDispatcher keyRedirecter = new KeyDispatcher(this);
  
  private boolean draggingListenersAdded = false;

  private boolean enableDragSelect = false;
  private boolean highlightingEnabled = false;

  private Rectangle initialVisibleRect;

  private Cursor cursorBeforeDrag;

  /**
   * Creates a pane with default {@link DraggableConstraints} configuration without drag box select
   * or highlighting. 
   * 
   * 
   */
  public DraggableLayeredPane() {
    this(false, false);
  }

  /**
   * 
   * @param constraints Describes the configuration of this pane when the user is dragging or resizing a component on the
   * pane.
   */
  public DraggableLayeredPane(DraggableConstraints constraints) {
    this(constraints, false, false);
  }

  /**
   * 
   * @param constraints Describes the configuration of this pane when the user is dragging or resizing a component on the
   * pane.
   * 
   * @param enableDragSelect True if the user can drag a box around the components to select them
   * @param highlightingEnabled True if the components can be highlighted
   */
  public DraggableLayeredPane(DraggableConstraints constraints, boolean enableDragSelect, boolean highlightingEnabled) {
    super();

    if (constraints != null) {
      this.constraints = constraints;
    }
    
    this.enableDragSelect = enableDragSelect;
    this.highlightingEnabled = highlightingEnabled;

    setDoubleBuffered(true);

    setFocusable(true);
    setOpaque(false);
  }

  /**
   * Creates a pane with default {@link DraggableConstraints} configuration. 
   * 
   * @param enableDragSelect True if the user can drag a box around the components to select them
   * @param highlightingEnabled True if the components can be highlighted
   */
  public DraggableLayeredPane(boolean enableDragSelect, boolean highlightingEnabled) {
    this(null, enableDragSelect, highlightingEnabled);
  }

  /**
   * Any component added with this method will be draggable by the mouse, but not resizable
   * 
   * @param component
   *          component to add
   * @param layer
   *          layer to add it to
   */
  public void addDraggableComponent(Component component, Integer layer) {
    addDraggableComponent(component, layer, false);
  }

  /**
   * Any component added with this method will be draggable by the mouse and optionally resizable.
   * 
   * @param component
   *          component to add
   * @param layer
   *          layer to add it to
   * @param resizable
   *          true if it should be possible to resize the component
   */
  public void addDraggableComponent(Component component, Integer layer, boolean resizable) {
    ensureDragListenersAdded();
    // Add the drag redirecter, the listener that transfers a drag on the component
    // to this component
    component.addMouseListener(dragRedirecter);
    component.addMouseMotionListener(dragRedirecter);
    
    // Redirect key presses from the new component to this panel
    component.addKeyListener(keyRedirecter);

    draggableComponents.add(component);

    if (resizable) {
      resizableComponents.add(component);
    }

    add(component, layer);
  }
  
  private void ensureDragListenersAdded() {
    if(!draggingListenersAdded) {
      /** Add the listeners */
      addMouseMotionListener(this);
      addKeyListener(this);
      
      draggingListenersAdded = true;
    }
  }

  /**
   * Any component added with this method will be draggable by the mouse and optionally resizable.
   * 
   * @param component
   *          component to add
   * @param layer
   *          layer to add it to
   * @param resizable
   *          true if it should be possible to resize the component
   */
  public void addDraggableComponent(SelectableComponent component, Integer layer, boolean resizable) {
    getSelectionManager().addComponent(component);

    addDraggableComponent(component.getComponent(), layer, resizable);
  }

  /**
   * Any component added with this method will be draggable by the mouse and optionally resizable.
   * 
   * @param component
   *          component to add
   * @param layer
   *          layer to add it to
   * @param resizable
   *          true if it should be possible to resize the component
   */
  public void addSelectableComponent(SelectableComponent component, Integer layer) {
    getSelectionManager().addComponent(component);

    add(component.getComponent(), layer);
  }

  /**
   * Removes a component from the panel. Makes sure all listeners etc are removed
   * 
   * @param component
   */
  public void remove(Component component) {
    // avoid NullPointerException in designer
    if (component == null) {
      return;
    }

    super.remove(component);
    draggableComponents.remove(component);

    // Remove the drag listener
    // (No harm done if there is no listener on the component)
    component.removeMouseListener(dragRedirecter);
    component.removeMouseMotionListener(dragRedirecter);

    if(selectionManager != null) {
      // Remove it from the selection manager
      // (No harm done if component isn't on the selection manager)
      selectionManager.removeComponent(component);
    }

    // Remove from our list of resizable components
    // (No harm done ... well you get the idea of that)
    resizableComponents.remove(component);
    if (dragComponent == component) {
      dragComponent = null;
    }
  }
  
  /**
   * PR 2147 
   */
  public void removeListeners() {
    /** Add the listeners */
    removeMouseMotionListener(this);
    removeKeyListener(this);    
  }  
  
  /**
   * Pr 2147
   */
  public void destroy() {
    draggableComponents.clear();
    dragComponent = null;
    resizableComponents.clear();
    componentLayers.clear();
  }

  /**
   * Called by an instance of DragRedirecter when a user tries to drag a node of the graph. Starts dragging that node
   * and redispatches the event to this component so we can handle it in terms of this pane.
   * 
   * @param component
   *          component being dragged
   * @param where
   *          the mouse event that started the drag
   */
  public void clickOccured(Component component, MouseEvent where) {
    // do nothing; for overloading
  }

  /**
   * Called by an instance of DragRedirecter when a user tries to drag a node of the graph. Starts dragging that node
   * and redispatches the event to this component so we can handle it in terms of this pane.
   * 
   * @param component
   *          component being dragged
   * @param where
   *          the mouse event that started the drag
   */
  public void draggingOccured(Component component, MouseEvent where) {
    
    if(selectionManager != null) {
      selectionManager.setDragSelectEnabled(false);
      selectionManager.setHighlightingEnabled(false);
    }

    // Are we not dragging already?
    if (dragComponent == null) {
      initialVisibleRect = getVisibleRect();
      // Start dragging this component
      dragComponent = component;
      
      cursorBeforeDrag = dragComponent.getCursor();

      if (selectionManager != null &&
          component instanceof SelectableComponent &&
          ((SelectableComponent) component).isSelected()) {
        componentLayers = new HashMap();
        Iterator nextSelectedComponent = selectionManager.getSelectedComponents().iterator();
        while (nextSelectedComponent.hasNext()) {
          Component currentComponent = ((SelectableComponent) nextSelectedComponent.next()).getComponent();
          componentLayers.put(currentComponent, new Integer(getLayer(currentComponent)));
          setLayer(currentComponent, highestLayer() + 1);
        }
      }

      if (resizingMode == NONE) {
        dragComponent.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
        setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
      }
      else {
        dragComponent.setCursor(Cursor.getPredefinedCursor(resizingMode));
        setCursor(Cursor.getPredefinedCursor(resizingMode));
      }

      currentlyDragging = true;
      // This is the difference between the mouse position and the components position
      initialDragOffset = where.getPoint();
      currentDragOffset = new Point(initialDragOffset);

      // Refresh the screen
      repaint();

    }

    // Convert the drag event and dispatch it to this component so we can deal
    // with it in our own listener
    dispatchEvent(SwingUtilities.convertMouseEvent(component, where, this));
  }

  /**
   * Called by DragRedirecter when we stop dragging a component
   * 
   * @param component
   */
  public void draggingStopped(Component component) {
    // Should we stop the use from dragging outside the bounds of the component?
    if (constraints.constrainInBoundsMin || constraints.constrainInBoundsMax) {
      constrainDragComponentWithinBounds();
    }

    // We're definitely not dragging
    resizingMode = NONE;
    // Reset the cursor
    setCursor(Cursor.getPredefinedCursor(NONE));
    dragComponent.setCursor(cursorBeforeDrag);

    Iterator nextSelectedComponent = componentLayers.keySet().iterator();
    while (nextSelectedComponent.hasNext()) {
      Component currentComponent = ((Component) nextSelectedComponent.next());
      setLayer(currentComponent, ((Integer) componentLayers.get(currentComponent)).intValue());
    }

    repaint();
    dragComponent = null;
    currentlyDragging = false;
    
    if(selectionManager != null) {
      selectionManager.setDragSelectEnabled(true);
      selectionManager.setHighlightingEnabled(true);
    }
  }

  /**
   * Drag listener used to drag nodes around the graph and to allow the user to scroll by dragging with the middle
   * button
   * 
   */
  public void mouseDragged(MouseEvent event) {
    // Are already dragging a component?
    if (dragComponent != null) {
      Rectangle boundsChange = dragComponent.getBounds();

      // Move the point at which we have dragged to
      currentDragPoint.setLocation(event.getPoint());

      Dimension previousSize = dragComponent.getSize();

      // Are we not resizing (so we're just dragging)
      if (resizingMode == NONE) {
        Rectangle newBounds = new Rectangle(dragComponent.getBounds());

        // Snap the dragged to point to the grid or the nearest other component
        Point snappedPoint = snapPoint(new Point((int) (currentDragPoint.getX() - currentDragOffset.x), (int) (currentDragPoint.getY() - currentDragOffset.y)));
        newBounds.x = snappedPoint.x;
        newBounds.y = snappedPoint.y;

        // Move the component according to that point
        dragComponent.setBounds(newBounds);
        
        if(constraints.scrollWhenDraggingOutOfBounds) {
          scrollAndAdjustOffset(newBounds);
        }

      }
      else {
        // Handle the resize
        previousSize = resizeOccured();
      }

      // Make sure the user hasn't dragged the component off the pane
      if (constraints.constrainInBoundsMin || constraints.constrainInBoundsMax) {
        constrainDragComponentWithinBounds(previousSize);
      }

      boundsChange = new Rectangle(dragComponent.getX() - boundsChange.x, dragComponent.getY() - boundsChange.y, dragComponent.getWidth() - boundsChange.width, dragComponent
          .getHeight()
          - boundsChange.height);

      if(selectionManager != null) {
        Iterator nextSelectedComponent = selectionManager.getSelectedComponents().iterator();
        while (nextSelectedComponent.hasNext()) {
          Component currentComponent = ((SelectableComponent) nextSelectedComponent.next()).getComponent();
  
          if (currentComponent != dragComponent) {
            Rectangle newBounds = currentComponent.getBounds();
            newBounds.x += boundsChange.x;
            newBounds.y += boundsChange.y;
  
            previousSize = newBounds.getSize();
  
            if (resizableComponents.contains(currentComponent)) {
              newBounds.width += boundsChange.width;
              newBounds.height += boundsChange.height;
            }
  
            currentComponent.setBounds(newBounds);
            currentComponent.validate();
  
            // Make sure the user hasn't dragged the component off the pane
            if (constraints.constrainInBoundsMin || constraints.constrainInBoundsMax) {
              constrainComponentWithinBounds(currentComponent, initialDragOffset, previousSize);
            }
          }
        }
      }

      repaint();
    }
  }

  /**
   * If the given bounds are not completely visible the will be scrolled
   * to make them visible.  Will also adjust the drag offset (offset between
   * the mouse and the component when we started dragging).  This enables a smooth
   * drag despite the fact that we are scrolling.
   * 
   * @param bounds
   */
  private void scrollAndAdjustOffset(Rectangle bounds) {
    // Do we need to scroll?
    if(!initialVisibleRect.contains(bounds)) {
      Rectangle scrollTo = new Rectangle(initialVisibleRect);
      int leftDiff = 0, rightDiff = 0, topDiff = 0, bottomDiff = 0;

      if(bounds.x > 0) {
        rightDiff =  (bounds.x + bounds.width) - (initialVisibleRect.x + initialVisibleRect.width);
        leftDiff =  initialVisibleRect.x - bounds.x;
      }
      
      if(bounds.y > 0) {
        bottomDiff =  (bounds.y + bounds.height) - (initialVisibleRect.y + initialVisibleRect.height);
        topDiff =  initialVisibleRect.y - bounds.y;
      }
      
      // Scrolling to the right?
      if(rightDiff > 0) {
        currentDragOffset.x = initialDragOffset.x + rightDiff;
        scrollTo.x += rightDiff;
      } else if(leftDiff > 0){
        currentDragOffset.x = initialDragOffset.x - leftDiff;
        scrollTo.x -= leftDiff;
      }

      // Scrolling down?
      if(bottomDiff > 0) {
        currentDragOffset.y = initialDragOffset.y + bottomDiff;
        scrollTo.y += bottomDiff;
      } else if(topDiff > 0){
        currentDragOffset.y = initialDragOffset.y - topDiff;
        scrollTo.y -= topDiff;
      }
      
      scrollRectToVisible(scrollTo);
    }
  }

  /**
   * Handles the current resize that is occuring as the user drags the mouse
   * 
   * @return the maximum size the user can resize to
   */
  private Dimension resizeOccured() {
    Dimension maxSize = new Dimension(getSize());

    // Make sure the cursor is appropriate
    setCursor(Cursor.getPredefinedCursor(resizingMode));

    Rectangle newBounds = new Rectangle(dragComponent.getBounds());

    Point approximatedDragPoint = snapPoint(new Point(currentDragPoint.x, currentDragPoint.y));

    // Are resizing from the top?
    if (resizingMode == NORTH || resizingMode == NORTH_EAST || resizingMode == NORTH_WEST) {

      // Move the component up by the size of the drag
      newBounds.y = (int) (approximatedDragPoint.getY());

      // Make the component taller by the size of the drag
      newBounds.height += (int) (dragComponent.getY() - approximatedDragPoint.getY());

      maxSize.height = dragComponent.getY() + dragComponent.getHeight();
    }
    // Are resizing from the top?
    else if (resizingMode == SOUTH || resizingMode == SOUTH_EAST || resizingMode == SOUTH_WEST) {
      // Make the component taller by the size of the drag
      newBounds.height = (int) (approximatedDragPoint.getY() - dragComponent.getY());

      maxSize.height = getHeight() - dragComponent.getY();
    }

    // Are resizing from the right?
    if (resizingMode == EAST || resizingMode == NORTH_EAST || resizingMode == SOUTH_EAST) {
      // Make the component wider by the size of the drag
      newBounds.width = (int) (approximatedDragPoint.getX() - dragComponent.getX());

      maxSize.width = getWidth() - dragComponent.getX();
    }
    // Are resizing from the left?
    else if (resizingMode == WEST || resizingMode == NORTH_WEST || resizingMode == SOUTH_WEST) {
      // Make the component wider by the size of the drag
      newBounds.width += (int) (dragComponent.getX() - approximatedDragPoint.getX());
      // Move the component left by the size of the drag
      newBounds.x = (int) (approximatedDragPoint.getX());

      maxSize.width = dragComponent.getX() + dragComponent.getWidth();
    }

    // Have we made the component too narrow?
    if (newBounds.width < dragComponent.getMinimumSize().width) {
      // Cancel the move
      newBounds.x = dragComponent.getX();
      newBounds.width = dragComponent.getMinimumSize().width;
    }

    // Have we made the component too narrow?
    if (newBounds.height < dragComponent.getMinimumSize().height) {
      // Cancel the move
      newBounds.y = dragComponent.getY();
      newBounds.height = dragComponent.getMinimumSize().height;
    }

    dragComponent.setBounds(newBounds);
    dragComponent.validate();
    dragComponent.repaint();

    return maxSize;
  }

  /**
   * Snaps the value to the nearest point on the grid of this pane.  For example if snapToGranularity is 5
   * and the value is 43, 45 will be returned.
   * 
   * @param value value to snap
   * @return value snapped to the nearest point on the grid.
   */
  private int snapValueToGrid(int value) {

    // divide down
    float valueToRound = ((float) value) / constraints.snapToGranularity;
    // round
    int roundedValue = Math.round(valueToRound);
    // multiply back up
    value = roundedValue * constraints.snapToGranularity;

    return value;
  }

  /**
   * Snaps the given point either to the nearest point on the grid or the nearest components, depending on the value
   * of constraints.snapToMode.
   * @param toSnap
   * @return
   */
  private Point snapPoint(Point toSnap) {
    Point result = new Point(toSnap);

    // These are the lines that were snapped to that must be painted on top of the components regular painting
    snapLineX = -1;
    snapLineY = -1;

    // Are we snapping to a grid that is of a sensible size?
    if (constraints.snapToMode == DraggableConstraints.SNAPTO_GRID && constraints.snapToGranularity > 1) {
      // Snap both x and y to the grid and store where we snapped to for painting later.
      result.x = snapValueToGrid(result.x);
      snapLineX = result.x;
      result.y = snapValueToGrid(result.y);
      snapLineY = result.y;
    }
    // Are we snapping to the other components on the pane?
    else if (constraints.snapToMode == DraggableConstraints.SNAPTO_COMPONENTS) {
      // This represents the distance to the nearest component on each axis
      int smallestGap = -1;

      // For each (draggable) component on the pane
      Iterator componentIterator = draggableComponents.iterator();
      while (componentIterator.hasNext()) {
        Component currentComponent = (Component) componentIterator.next();

        // Is this component not selected (don't snap to another component that we are dragging through
        // multiple select))
        if (selectionManager == null || !selectionManager.getSelectedComponents().contains(currentComponent)) {

          // Work out the difference to snap to the left of the component
          int xDifference = Math.abs(toSnap.x - currentComponent.getX());

          // Is this snap within the granularity?
          if (xDifference < constraints.snapToGranularity) {
            // Is this closer than the last snap we found?
            if (smallestGap == -1 || xDifference < smallestGap) {
              // Remember we made this snap
              smallestGap = xDifference;

              // Make the snap for now, (we may find a better snap)
              result.x = currentComponent.getX();
              snapLineX = result.x;
            }
          }

          // Work out the difference to snap to the right of the component
          xDifference = Math.abs(toSnap.x - (currentComponent.getX() + currentComponent.getWidth()));

          // Is this snap within the granularity?
          if (xDifference < constraints.snapToGranularity) {
            // Is this closer than the last snap we found?
            if (smallestGap == -1 || xDifference < smallestGap) {
              // Remember we made this snap
              smallestGap = xDifference;

              // Make the snap for now, (we may find a better snap)
              result.x = (currentComponent.getX() + currentComponent.getWidth());
              snapLineX = result.x;
            }
          }

          // Are we moving, not resizing the component?
          if (resizingMode == NONE) {
            xDifference = Math.abs((toSnap.x + dragComponent.getWidth()) - (currentComponent.getX() + currentComponent.getWidth()));

            // Is this snap within the granularity?
            if (xDifference < constraints.snapToGranularity) {
              // Is this closer than the last snap we found?
              if (smallestGap == -1 || xDifference < smallestGap) {
                // Remember we made this snap
                smallestGap = xDifference;

                // Make the snap for now, (we may find a better snap)
                result.x = (currentComponent.getX() + currentComponent.getWidth()) - dragComponent.getWidth();
                snapLineX = (currentComponent.getX() + currentComponent.getWidth());
              }
            }

            // Work out the difference to snap from the right of the drag component to the left of the component
            xDifference = Math.abs((toSnap.x + dragComponent.getWidth()) - (currentComponent.getX()));

            // Is this snap within the granularity?
            if (xDifference < constraints.snapToGranularity) {
              // Is this closer than the last snap we found?
              if (smallestGap == -1 || xDifference < smallestGap) {
                // Remember we made this snap
                smallestGap = xDifference;

                // Make the snap for now, (we may find a better snap)
                result.x = currentComponent.getX() - dragComponent.getWidth();
                snapLineX = currentComponent.getX();
              }
            }
          }
        }
      }

      // Repeat above for y axis

      smallestGap = -1;

      // For each (draggable) component on the pane
      componentIterator = draggableComponents.iterator();
      while (componentIterator.hasNext()) {
        Component currentComponent = (Component) componentIterator.next();

        // Is this component not selected (don't snap to another component that we are dragging through
        // multiple select))
        if (selectionManager == null || !selectionManager.getSelectedComponents().contains(currentComponent)) {
          // Work out the difference to snap to the top of the component
          int yDifference = Math.abs(toSnap.y - currentComponent.getY());

          // Is this snap within the granularity?
          if (yDifference < constraints.snapToGranularity) {
            // Is this closer than the last snap we found?
            if (smallestGap == -1 || yDifference < smallestGap) {
              // Remember we made this snap
              smallestGap = yDifference;

              // Make the snap for now, (we may find a better snap)
              result.y = currentComponent.getY();
              snapLineY = result.y;
            }
          }

          // Work out the difference to snap to the bottom of the component
          yDifference = Math.abs(toSnap.y - (currentComponent.getY() + currentComponent.getHeight()));

          // Is this snap within the granularity?
          if (yDifference < constraints.snapToGranularity) {
            // Is this closer than the last snap we found?
            if (smallestGap == -1 || yDifference < smallestGap) {
              // Remember we made this snap
              smallestGap = yDifference;

              // Make the snap for now, (we may find a better snap)
              result.y = (currentComponent.getY() + currentComponent.getHeight());
              snapLineY = result.y;
            }
          }

          // Are we moving, not resizing the component?
          if (resizingMode == NONE) {
            // Work out the difference to the bottom of the component from the bottom of the drag component
            yDifference = Math.abs((toSnap.y + dragComponent.getHeight()) - (currentComponent.getY() + currentComponent.getHeight()));
            // Is this snap within the granularity?
            if (yDifference < constraints.snapToGranularity) {
              // Is this closer than the last snap we found?
              if (smallestGap == -1 || yDifference < smallestGap) {
                // Remember we made this snap
                smallestGap = yDifference;

                // Make the snap for now, (we may find a better snap)
                result.y = (currentComponent.getY() + currentComponent.getHeight()) - dragComponent.getHeight();
                snapLineY = (currentComponent.getY() + currentComponent.getHeight());
              }
            }

            // Work out the difference to the top of the component from the bottom of the drag component
            yDifference = Math.abs((toSnap.y + dragComponent.getHeight()) - currentComponent.getY());
            // Is this snap within the granularity?
            if (yDifference < constraints.snapToGranularity) {
              // Is this closer than the last snap we found?
              if (smallestGap == -1 || yDifference < smallestGap) {
                // Remember we made this snap
                smallestGap = yDifference;

                // Make the snap for now, (we may find a better snap)
                result.y = currentComponent.getY() - dragComponent.getHeight();
                snapLineY = currentComponent.getY();
              }
            }
          }
        }
      }
    }

    return result;
  }

  /**
   * Constrains the currently dragged component inside the bounds of this component but does not affect the dimensions.
   */
  private void constrainDragComponentWithinBounds() {
    constrainDragComponentWithinBounds(null);

  }

  /**
   * Should be called whilst dragging is occuring. If the component currently being dragged is smaller than the pane on
   * a particular axis (i.e width, height) its location on that axis (i.e x,y) is adjusted so that the entire component
   * fits within the pane. If it is larger than the pane, it is adjusted so that it goes no further outside the screen
   * than the point where the user started dragging.
   * 
   */
  public void constrainDragComponentWithinBounds(Dimension previousSize) {
    // Are we really dragging?
    if (dragComponent != null) {
      constrainComponentWithinBounds(dragComponent, initialDragOffset, previousSize);
    }
  }

  /**
   * If the component is smaller than the pane on a particular axis (i.e width, height) its location on that axis (i.e
   * x,y) is adjusted so that the entire component fits within the pane. If a previous dimension is supplied (not null)
   * it is assumed that the user was resizing the component and this is the size to reset the component to. If it is
   * larger than the pane, it is adjusted so that it goes no further outside the screen than the offset.
   * 
   * @param component
   *          Component whose bounds are affected
   */
  public void constrainComponentWithinBounds(Component component, Point offset, Dimension previousDimension) {
    if (previousDimension != null && !constraints.childrenCanBeLarger) {
      previousDimension.width = Math.min(getWidth(), previousDimension.width);
      previousDimension.height = Math.min(getHeight(), previousDimension.height);
    }

    if (component != null) {
      Point location = component.getLocation();

      // Is the component wider than this container or do we not care?
      if (component.getWidth() < getWidth() || !constraints.childrenCanBeLarger) {
        // Is the component off the of this container?
        if (component.getX() < 0 && constraints.constrainInBoundsMin) {
          // Put the component only as far left as it will go
          location.x = 0;

          // Do we need to reset the width?
          if (previousDimension != null) {
            // Reset the width of the component
            component.setSize(previousDimension.width, component.getHeight());
          }
        }

        // Is the component off the right of this container
        if (component.getX() + component.getWidth() > getWidth() && constraints.constrainInBoundsMax) {
          // Do we need to reset the width?
          if (previousDimension != null) {
            // Reset the width of the component
            component.setSize(previousDimension.width, component.getHeight());
          }

          // Is the component still off the right of this container?
          if (component.getX() + component.getWidth() > getWidth()) {
            // Put the component only as far right as it will go
            location.x = getWidth() - component.getWidth();
          }
        }
      }
      // Component is larger than this container
      else {
        // Is the component further off left of this container than the given offset?
        if ((component.getX() + offset.x) < 0) {
          // keep the component within the given offset
          location.x = 0 - offset.x;
        }

        // Is the component further off right of this container than the given offset?
        if ((component.getX() + offset.x) > getWidth()) {
          // keep the component within the given offset
          location.x = getWidth() - offset.x;
        }
      }

      // Is the component higher than this or do we not care?
      if (component.getHeight() < getHeight() || !constraints.childrenCanBeLarger) {
        // Is the component off the top of this container
        if (component.getY() < 0 && constraints.constrainInBoundsMin) {
          // Reset to the top of this container
          location.y = 0;

          // Do we need to reset the height?
          if (previousDimension != null) {
            // Reset the height
            component.setSize(component.getWidth(), previousDimension.height);
          }
        }

        // Is the component off the bottom of this container
        if (component.getY() + component.getHeight() > getHeight() && constraints.constrainInBoundsMax) {
          // Do we need to reset the height
          if (previousDimension != null) {
            // Reset the height
            component.setSize(component.getWidth(), previousDimension.height);
          }

          // Is the component still off the bottom of this container
          if (component.getY() + component.getHeight() > getHeight()) {
            // Put the component only as far down as it will go
            location.y = getHeight() - component.getHeight();
          }
        }
      }
      // Component is larger than this container
      else {
        // Is the component further off the top of this container than the given offset?
        if ((component.getY() + offset.y) < 0) {
          // keep the component within the given offset
          location.y = 0 - offset.y;
        }

        // Is the component further off the bottom of this container than the given offset?
        if ((component.getY() + offset.y) > getHeight()) {
          // keep the component within the given offset
          location.y = getHeight() - offset.y;
          ;
        }
      }

      // Apply the location change
      component.setLocation(location);
    }

  }

  /**
   * Aggregation of listeners that should be assigned to any component that needs to be dragged. Passes the drag action
   * on to the main component.
   * 
   * @author tomm
   * 
   */
  private class DragRedirecter implements MouseListener, MouseMotionListener {
    public void mouseDragged(MouseEvent e) {
      boolean respectDrag = true;
      
      if(!e.isShiftDown()) {
        // Is the dragged component an instance of this class?
        if (e.getComponent() instanceof DraggableLayeredPane) {
          DraggableLayeredPane draggedPane = (DraggableLayeredPane) e.getComponent();
          // Is a drag already occuring inside the dragged pane?
          respectDrag = !draggedPane.isCurrentlyDragging();
        }
        // Are we responding to this drag?
        if (respectDrag) {
          if (e.getComponent() instanceof SelectableComponent && ((SelectableComponent) e.getComponent()).isSelected()) {
  
            if (!isCurrentlyDragging() && (selectionManager == null || !selectionManager.isDragging())) {
              if (resizingMode != NONE) {
                setCursor(Cursor.getPredefinedCursor(resizingMode));
              }
              else {
                setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
              }
            }
          }
          // by the DraggableLayeredPane instance
          draggingOccured(e.getComponent(), e);
  
          // Make sure the component is on top of its layer
          moveToFront(e.getComponent());
        }
      }
    }

    public void mouseMoved(MouseEvent e) {
      if (resizableComponents.contains(e.getComponent()) && !isCurrentlyDragging() && (selectionManager == null || !selectionManager.isDragging())) {
        int resizePosition = getResizePosition(e.getComponent(), e.getPoint());
        e.getComponent().setCursor(Cursor.getPredefinedCursor(resizePosition));
      }
      else {
        if (resizingMode == NONE) {
          e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
        }
        else {
          e.getComponent().setCursor(Cursor.getPredefinedCursor(resizingMode));
        }
      }
    }

    public void mouseClicked(MouseEvent e) {
      // Make sure the component is on top of its layer
      moveToFront(e.getComponent());
      clickOccured(e.getComponent(), e);
    }

    public void mouseEntered(MouseEvent e) {
      if (currentlyDragging) {
        if (resizingMode == NONE) {
          e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
        }
        else {
          e.getComponent().setCursor(Cursor.getPredefinedCursor(resizingMode));
        }
      } else {
        e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      }
    }

    public void mouseExited(MouseEvent e) {
      if(!currentlyDragging) {
        e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      }
    }

    public void mousePressed(MouseEvent e) {
      dragComponent = null;
      moveToFront(e.getComponent());

      if (resizableComponents.contains(e.getComponent())) {
        resizingMode = getResizePosition(e.getComponent(), e.getPoint());
      }

    }

    public void mouseReleased(MouseEvent e) {
      if (!e.isConsumed()) {
        // Announce that we're definetely not dragging
        draggingStopped(e.getComponent());
        moveToFront(e.getComponent());
        dragComponent = null;
      }

      resizingMode = NONE;
    }
  }

  public void mouseMoved(MouseEvent arg0) {

  }

  public boolean isCurrentlyDragging() {
    return currentlyDragging;
  }

  public Component getDragComponent() {
    return dragComponent;
  }

  public Dimension getPreferredSize() {
    Dimension result = super.getPreferredSize();

    Component[] components = getComponents();

    for (int componentIndex = 0; componentIndex < components.length; componentIndex++) {

      result.width = Math.max(components[componentIndex].getX() + components[componentIndex].getWidth(), result.width);
      result.height = Math.max(components[componentIndex].getY() + components[componentIndex].getHeight(), result.height);
    }

    return result;
  }

  public Component add(Component comp) {
    // comp.addMouseListener(focusMouseListener);
    return super.add(comp);
  }

  /**
   * Overriden so that components will can have a focus mouse listener added to them.
   */
  public void add(Component comp, Object layer) {
    // comp.addMouseListener(focusMouseListener);
    super.add(comp, layer);
  }

  /**
   * 
   * Calculates the edge the user intends to resize a component from by checking the position of the mouse.
   * 
   * @param component
   *          Component that is being resized
   * @param point
   *          where the mouse
   * @return NONE if the mouse is not in a resize position, otherwise NORTH (for the top edge), NORTH_EAST (for the top
   *         right corner), etc.
   */
  public final static int getResizePosition(final Component component, final Point point) {
    // Represents the top of the component
    Rectangle north = new Rectangle(0, 0, component.getWidth(), 5);
    // Represents the right of the component
    Rectangle east = new Rectangle(component.getWidth() - 5, 0, 5, component.getHeight());
    // Represents the left of the component
    Rectangle west = new Rectangle(0, 0, 5, component.getHeight());
    // Represents the south of the component
    Rectangle south = new Rectangle(0, component.getHeight() - 5, component.getWidth(), 5);

    // Top of the component?
    if (north.contains(point)) {
      // Top right?
      if (east.contains(point)) {
        return NORTH_EAST;
      }
      // Top left
      else if (west.contains(point)) {
        return NORTH_WEST;
      }
      else {
        return NORTH;
      }
    }
    else if (south.contains(point)) {
      if (east.contains(point)) {
        return SOUTH_EAST;
      }
      else if (west.contains(point)) {
        return SOUTH_WEST;
      }
      else {
        return SOUTH;
      }
    }
    else if (west.contains(point)) {
      return WEST;
    }
    else if (east.contains(point)) {
      return EAST;
    }
    else {
      return NONE;
    }
  }

  public SelectionManager getSelectionManager() {
    if(selectionManager == null) {
      selectionManager = new SelectionManager(this, enableDragSelect, highlightingEnabled);
    }
    return selectionManager;
  }

  public void setSelectionManager(SelectionManager selectionManager) {
    
    // Remove previous selection manager if not null
    if(this.selectionManager != null) {
      this.selectionManager.removeFromContainer(this);
    }
    
    this.selectionManager = selectionManager;
  }

  public void paint(Graphics g) {
    super.paint(g);

    if (currentlyDragging && constraints.drawingGuidelines) {

      Graphics2D clone = (Graphics2D) g.create();

      clone.setColor(constraints.guideColor);

      clone.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, constraints.guideOpacity));
      clone.setStroke(new BasicStroke(1f));

      if (snapLineX != -1) {
        clone.drawLine(snapLineX, 0, snapLineX, getHeight());
      }

      if (snapLineY != -1) {
        clone.drawLine(0, snapLineY, getWidth(), snapLineY);
      }

      // Are drawing a grid (with a cell size greater than one pixel)
      if (constraints.snapToMode == DraggableConstraints.SNAPTO_GRID && constraints.snapToGranularity > 1) {
        // Is the cached image of the grid out of date (We cache the image of a grid for efficiency when painting guidelines)
        if (isGridCacheOutOfDate()) {
          // Create a new image
          gridCache = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
          
          // Remember the settings so if we change them we know to redraw the cache
          cachedGridColor = constraints.guideColor;
          cachedGridSize = constraints.snapToGranularity;
          
          // Get a graphics object for the image and set it to the right color
          Graphics imageGraphics = gridCache.getGraphics();
          imageGraphics.setColor(constraints.guideColor);

          // Draw horizontal lines
          for (int x = 0; x < getWidth(); x += constraints.snapToGranularity) {
            imageGraphics.drawLine(x, 0, x, getHeight());
          }

          // Draw vertical lines
          for (int y = 0; y < getHeight(); y += constraints.snapToGranularity) {
            imageGraphics.drawLine(0, y, getWidth(), y);
          }
        }
        
        clone.drawImage(gridCache, 0, 0, this);
      }

      clone.dispose();
    }
  }
  
  /**
   * We cache the image of a grid for efficiency when painting guidelines.  However if the grid changes
   * cell size or color or if this component changes size we need to redraw the cache.  This method checks to see
   * if the grid cache is still valid.
   * 
   * @return true if the cache does not need to be redrawn (or if it is currently null) else false
   */
  private boolean isGridCacheOutOfDate() {
    return  gridCache == null ||
            gridCache.getWidth(this) != getWidth() || 
            gridCache.getHeight(this) != getHeight() || 
            constraints.guideColor.equals(cachedGridColor) ||
            constraints.snapToGranularity != cachedGridSize;
  }

  public DraggableConstraints getConstraints() {
    return constraints;
  }

  public void setDraggableConstraints(DraggableConstraints constraints) {
    this.constraints = constraints;
  }

  /**
   * When the cursor keys are pressed the selected components should be moved by the
   * grid granularity
   */
  public void keyPressed(KeyEvent e) {
    // Are we moving with the keyboard?
    if(this.constraints.moveWithKeys) {
      
      int keyCode = e.getKeyCode();
  
      // Amount the components should be moved 
      Point translation = new Point();
  
      // was the up key pressed?
      if (keyCode == constraints.upKey) {
        // Move the components up
        translation.y -= constraints.snapToGranularity;
      }
      //etc.
      else if (keyCode == constraints.downKey) {
        translation.y = constraints.snapToGranularity;
      }
      else if (keyCode == constraints.leftKey) {
        translation.x -= constraints.snapToGranularity;
      }
      else if (keyCode == constraints.rightKey) {
        translation.x += constraints.snapToGranularity;
      }
      
      // Did we actually press a move key
      if((translation.x != 0 || translation.y != 0) &&
          selectionManager != null) {
  
        // For each selected component
        Iterator nextSelectedComponent = selectionManager.getSelectedComponents().iterator();
        while (nextSelectedComponent.hasNext()) {
          Component currentComponent = ((SelectableComponent) nextSelectedComponent.next()).getComponent();
    
          Rectangle bounds = currentComponent.getBounds();
    
          // Apply translation
          bounds.y += translation.y;
          bounds.x += translation.x;
    
          // Are we not allowed to move components off the top or left of the panel?
          if (constraints.constrainInBoundsMin) {
            // Constrains so left/top edge of component is inside this panel
            bounds.x = Math.max(bounds.x, 0);
            bounds.y = Math.max(bounds.y, 0);
          }
          
          // Are we not allowed to move components off the bottom or right of the panel?
          if (constraints.constrainInBoundsMax) {
            // Constrains so right/bottom edge of component is inside this panel
            bounds.x = Math.min(bounds.x, getWidth() - bounds.width);
            bounds.y = Math.min(bounds.y, getHeight() - bounds.height);
          }
    
          // Apply new bounds
          currentComponent.setBounds(bounds);
        }
      }
    }
  }

  public void keyReleased(KeyEvent e) {
    
  }

  public void keyTyped(KeyEvent e) {
    
  }
}
